In [1]:
%load_ext autoreload
%autoreload 2

import sys
from pathlib import Path
import numpy as np

import pandas as pd
import zfit_modelcore_cli as zfitcli
from zfit_plot import LogLogPlotDualAxisPlotly

The following code is used adapting the python script Zfit2.0, tuning it to be possible to call it from CLI (command line interface) and from python notebook to be launched also online platform. the following code line permit to run the original code available at https://exality.com/fitting-equivalent-circuits-to-impedance-data/

In [2]:
# !python Zfit.pyw

Example fitting L+C//R

this example is useful in power electronic because is the typical output stage for a buck DC/DC stage.

Measuring it between the IN node and GND, then we can compare the meas. with the theoretical values and verify if there are discrepancies due to manufacturing.

image.png

In [3]:
# Test case
# DESCRIPTION: load example Zfit
# EXPECTED RESULT: fitting correct in plotly
filename_choke=r"./TestDataFiles/Sample ls(cpr).csv"
df_LCR = pd.read_csv(filename_choke)
#df_LCR.columns
In [4]:
range = zfitcli.Range()
ya = zfitcli.YAxes(2)

range.xa['Hz'] = df_LCR['FREQUENCY [Hz]']
ya.inputData[0] = df_LCR['MAG [Ω]']
ya.inputData[1] = df_LCR['PHASE [deg]']

my_model = zfitcli.DoModel()
fit_model = zfitcli.import_module("Models." + "ls(cpr)")
my_model.do_model_cli(range=range, ya=ya, model=fit_model)
Ls, 7.7291966061495e-08
Cp, 9.688648081088437e-10
Rp, 193.7836211042093
Number of function calls: 37
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 37
    # data points      = 802
    # variables        = 3
    chi-square         = 0.03036534
    reduced chi-square = 3.8004e-05
    Akaike info crit   = -8159.61267
    Bayesian info crit = -8145.55134
[[Variables]]
    Ls:  0.02444187 +/- 2.1822e-05 (0.09%) (init = 0.03162278)
    Cp:  3.06381954 +/- 0.00264503 (0.09%) (init = 31.62278)
    Rp:  1.93783621 +/- 0.00225481 (0.12%) (init = 1)
[[Correlations]] (unreported correlations are < 0.100)
    C(Ls, Cp) = -0.943

From the graph below is visible that at low frequency the total impedance of the buck filter is just given by the load. This is why, in any PWM converter, the LC filter does not influence the DC transfer function.

After $f=300kHz$ the effect of the capacitor reduce the input impedance, till the resonance happen and the impedance start to be inductive again, increasing it with the frequency.

How the curve look like depends from the combination of $R, C, L$. If $L >> C$ then the curve will start to rise and then fall (exact the opposite fo this case)

In [5]:
newplot=LogLogPlotDualAxisPlotly()
newplot.add_trace(f=df_LCR['FREQUENCY [Hz]'], 
                  zabs=df_LCR['MAG [Ω]'], zdeg=df_LCR['PHASE [deg]'], 
                  label="Sample ls(cpr)")
newplot.add_trace(f=df_LCR['FREQUENCY [Hz]'], 
                  zabs=my_model.ya.modeledData[0], zdeg=my_model.ya.modeledData[1], 
                  label="FIT Sample ls(cpr)")
newplot.show()

Example fit inductors Rdc+L//Rproximity//Cp

This is the typical fitting necessary with any power inductor. the simplest model used by many manufacturer to take in cosideration the increase of the losses with the frequency is model.jpg

The 4 components have different purpose:

  1. RDC fits the DC losses, this can be measured to with a normal 4W method or letting flow some DC current and measuring the voltage across the choke

  2. Rproximity is a resistance placed in the electrical model in parallel to the inductance to model the AC losses due to proximity effect, this is because the losses raise with $f^2$

  3. L is the inductance measured at low frequency (this value reduce if the frequency increases due to the eddy currents)

  4. C is the equivalent total capacitance seen at the terminal (this include intra-winding and terminal capacitance)

If we extract the real part and the imaginary part of the complex impedance we can see that look like the following:

Re{Z} and Im{Z} equation Re{Z} and Im{Z} equation

the imaginary part (what normally in attributed just to the inductance) drop with the frequency, and the real part rise with $f^2$. It's also worth to mention that if we set $Rp=0$, the model comes back to the simple $RL$ series (the simples inductor model possible)

Inverter inductor fitting

In the next section, we will fit the LCR measurements from 3 different inverter inductors, produced with different techniques:

  1. the one on the left is produced by the company Sirio SRL http://www.sirio-ic.com/index.php/en/
  2. the one in the center is winding by my collegueas at NTBees
  3. the one on the right is produced by Kasche Components https://kaschke.de/

NTB inductors set

Sirio inductor fitting

in the next section we will fit the measurement of a Sirio inductor, used for a 3kW solar inverter.

In [16]:
# Test case
# DESCRIPTION: load coilcraft inductors and calc loss factor at different frequencies
# EXPECTED RESULT: new excel file with the same db and loss factor

filename_choke_sirio=r"./TestDataFiles/Inductor2.xls"
sheetname_choke_sirio="sirio"

# df_sirio = pd.read_excel(io=filename_choke, sheet_name=sheetname_choke_sirio)
df_sirio = pd.read_excel(io=filename_choke_sirio, sheet_name=sheetname_choke_sirio)

#R+j*X
f = df_sirio['Frequency (Hz)']
L = df_sirio['Inductance (H)']   
R = df_sirio['Resistance (O)']

# we calc complex impedance abs(Z)*exp(j*deg)
Zabs_sirio = np.abs(R+1j*2*np.pi*f*L)
Zdeg_sirio = np.angle(R+1j*2*np.pi*f*L)

Fitting the measurements with a simple $RLC$ parallel we can obtain the following values:

In [7]:
# prepare input for the function
range.xa['Hz'] = f
ya.inputData[0] = Zabs_sirio
ya.inputData[1] = Zdeg_sirio

# do the fitting
my_model = zfitcli.DoModel()
fit_model = zfitcli.import_module("Models." + "rpcpl")
#fit_model = zfitcli.import_module("Models." + "rs(rpcpl)")
#fit_model = zfitcli.import_module("Models." + "rpcp(lsr_skin)")
#fit_model = zfitcli.import_module("Models." + "cpl_f")

# do_model_cli(self, range, ya, model, method_nr=0, do_norm_denorm=True):
my_model.do_model_cli(range=range, ya=ya, model=fit_model, method_nr=1)
R, 1051.4701112288435
C, 1.4497395774645852e-11
L, 0.000149063907937561
Number of function calls: 14
[[Fit Statistics]]
    # fitting method   = least_squares
    # function evals   = 14
    # data points      = 1602
    # variables        = 3
    chi-square         = 285.698952
    reduced chi-square = 0.17867352
    Akaike info crit   = -2755.95932
    Bayesian info crit = -2739.82229
[[Variables]]
    R:  1.05147011 +/- 0.07235499 (6.88%) (init = 1)
    C:  0.18716057 +/- 0.02556133 (13.66%) (init = 0.5163978)
    L:  1.92440678 +/- 0.07754427 (4.03%) (init = 1.936492)
[[Correlations]] (unreported correlations are < 0.100)
    C(C, L) = -0.123
In [8]:
# compare the real parts of the impedance
# the measured one
# the real measurements from sirio inductor
Z=Zabs_sirio*np.exp(1j*Zdeg_sirio)
ReZ_sirio=np.real(Z)
newplot=LogLogPlotDualAxisPlotly()
newplot.add_trace(f=f, zabs=ReZ_sirio, zdeg=None, label="meas_SirioInductor")

params={"Rs":0.015, "Rp":9108.42301217358, "L":0.00015606072332020876, "C":2.5235270544381366e-11}
fit_model = zfitcli.import_module("Models." + "rs(rpcpl)")
ReZ_fit = np.real(fit_model.model(w=2*np.pi*f, params=params))
newplot.add_trace(f=f, zabs=ReZ_fit, zdeg=None, label="fit_rpcpl_SirioInductor")

newplot.show()

As visible from the fitting above, the model cannot fit behaviors of real part lower than $f^2$. The fit will overestimate the losses for a large range of frequency. the same is visible in the magnitude/phase plot:

In [9]:
params={"Rs":0.015, "Rp":9108.42301217358, "L":0.00015606072332020876, "C":2.5235270544381366e-11}
fit_model = zfitcli.import_module("Models." + "rs(rpcpl)")
Z_fit = fit_model.model(w=2*np.pi*f, params=params)

newplot=LogLogPlotDualAxisPlotly()
newplot.add_trace(f=f, zabs=Zabs_sirio, zdeg=Zdeg_sirio*180/np.pi, label="SirioInductors")
newplot.add_trace(f=f, zabs=np.abs(Z_fit), zdeg=np.angle(Z_fit)/np.pi*180, label="FIT_SirioInductors")
newplot.show()

To improve this we can use the model that include a real part that increase with $f$.

In [10]:
# compare the real parts of the impedance
# the measured one
# the real measurements from sirio inductor
Z=Zabs_sirio*np.exp(1j*Zdeg_sirio)
ReZ_sirio=np.real(Z)
newplot=LogLogPlotDualAxisPlotly()
newplot.add_trace(f=f, zabs=ReZ_sirio, zdeg=None, label="meas_SirioInductor")

# # the fitted ones
# params={"Rldc":0.01, "L":4e-5, "C":1e-10, "sf":1e-6}
# # params={"Rldc":1e-2, "L":0.00015606072332020876, "C":2.5235270544381366e-11, "sf":1e-6}
# fit_model = zfitcli.import_module("Models." + "cpl_f")
# ReZ_fit = np.real(fit_model.model(w=2*np.pi*f, params=params))
# newplot.add_trace(f=f, zabs=ReZ_fit, zdeg=None, label="fit_cpl_f_SirioInductor")

# the fitted ones
params={"Rs":16.6434734, "L":4e-5, "C":1e-10, "sf":1e-6}
# params={"Rldc":1e-2, "L":0.00015606072332020876, "C":2.5235270544381366e-11, "sf":1e-6}
fit_model = zfitcli.import_module("Models." + "cpl_f")
ReZ_fit = np.real(fit_model.model(w=2*np.pi*f, params=params))
newplot.add_trace(f=f, zabs=ReZ_fit, zdeg=None, label="fit_cpl_f_SirioInductor")

newplot.show()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-10-9248ba392406> in <module>
     18 # params={"Rldc":1e-2, "L":0.00015606072332020876, "C":2.5235270544381366e-11, "sf":1e-6}
     19 fit_model = zfitcli.import_module("Models." + "cpl_f")
---> 20 ReZ_fit = np.real(fit_model.model(w=2*np.pi*f, params=params))
     21 newplot.add_trace(f=f, zabs=ReZ_fit, zdeg=None, label="fit_cpl_f_SirioInductor")
     22 

D:\OneDrive\NTB\105\06_Simulationen\ntbees\classes\utils\pyZfit\Models\cpl_f.py in model(w, params, **kws)
     29     C = params['C']
     30     L = params['L']
---> 31     Rldc = params['Rldc']
     32     sf = params['sf']
     33     # This is the definition of the model impedance which we want to

KeyError: 'Rldc'

Fitting of different inductors

In the webinar from Omicron on 2020.07.27, they analyzed 4 different types of inductors:

  1. SMD flat bad inductor

  2. litz wire inductor

  3. copper wire inductor

  4. planar coils (PCB winding structure)

for more details: https://www.youtube.com/watch?v=T2OqewIUL3M&list=PLE3Pq-hBtiMRyV62bv2_UoKQ-YofQI5IB&index=4 inductor%20overview.PNG.

What is interesting to compare is the real impedance of each type of inductor and see what are the best application for each inductor.

In [11]:
# load the data first from the Bode100 export

filename_chokes_webinar=r"./TestDataFiles/Inductors_2020-07-27T09_29_28.xlsx"
df_chokes = pd.read_excel(io=filename_chokes_webinar)
df_chokes.columns
Out[11]:
Index(['Frequency (Hz)', 'Unnamed: 1', 'FlatBandWire: Frequency (Hz)',
       'FlatBandWire: Trace 1: Magnitude (Ω)',
       'FlatBandWire: Trace 2: Phase (°)', 'Unnamed: 5',
       'PCB LitzEmulation: Frequency (Hz)',
       'PCB LitzEmulation: Trace 1: Magnitude (Ω)',
       'PCB LitzEmulation: Trace 2: Phase (°)', 'Unnamed: 9',
       'RF Litz Wire: Frequency (Hz)', 'RF Litz Wire: Trace 1: Magnitude (Ω)',
       'RF Litz Wire: Trace 2: Phase (°)', 'Unnamed: 13',
       'SolidStrand: Frequency (Hz)', 'SolidStrand: Trace 1: Magnitude (Ω)',
       'SolidStrand: Trace 2: Phase (°)'],
      dtype='object')
In [21]:
f=df_chokes['Frequency (Hz)']
Zabs_flatwire=df_chokes['FlatBandWire: Trace 1: Magnitude (Ω)']
Zdeg_flatwire=df_chokes['FlatBandWire: Trace 2: Phase (°)']
Z_flatwire=Zabs_flatwire*np.exp(1j*Zdeg_flatwire*np.pi/180)

Zabs_planar=df_chokes['PCB LitzEmulation: Trace 1: Magnitude (Ω)']
Zdeg_planar=df_chokes['PCB LitzEmulation: Trace 2: Phase (°)']
Z_planar=Zabs_planar*np.exp(1j*Zdeg_planar*np.pi/180)

Zabs_RFlitz=df_chokes['RF Litz Wire: Trace 1: Magnitude (Ω)']
Zdeg_RFlitz=df_chokes['RF Litz Wire: Trace 2: Phase (°)']
Z_RFlitz=Zabs_RFlitz*np.exp(1j*Zdeg_RFlitz*np.pi/180)

Zabs_SolidStrand=df_chokes['SolidStrand: Trace 1: Magnitude (Ω)']
Zdeg_SolidStrand=df_chokes['SolidStrand: Trace 2: Phase (°)']
Z_SolidStrand=Zabs_SolidStrand*np.exp(1j*Zdeg_SolidStrand*np.pi/180)

It's interesting to see how different look like the behavior of the inductors, even if at $f=100kHz$ are identical. This is where FRA, like the Bode100 can make the difference, showing the behavior of the complex impedance of the inductors. some considerations we can do, already seeing the complex impedances:

  1. the flat band inductor, even if the intrawinding surface is the biggest between the 4, has the highest resonant freq. Due to the airgap between the winding itself.
  2. similar concept is for the planar PCB, that normally many datasheet point as affected by high parasitics
  3. the litz wire inductor has the lowest resonance. At $f>5MHz$ this shows impedance an order of magnitude lower than flat band
In [22]:
newplot=LogLogPlotDualAxisPlotly()
newplot.add_trace(f=f, zabs=Zabs_flatwire, zdeg=Zdeg_flatwire, label="FlatBandWire")
newplot.add_trace(f=f, zabs=Zabs_planar, zdeg=Zdeg_planar, label="LitzEmulation")
newplot.add_trace(f=f, zabs=Zabs_RFlitz, zdeg=Zdeg_RFlitz, label="RF Litz Wire")
newplot.add_trace(f=f, zabs=Zabs_SolidStrand, zdeg=Zdeg_SolidStrand, label="SolidStrand")
newplot.show()

It's even more interesting to look at real part of the inductors. as visible from the following picture:

  1. the Litz wire is the best until $f<577kHz$ then the PCB inductor wins. the behavior at high freq is worse than then other because present multiple resonances. At DC $f=0$ is the second best choice.

  2. Solid strand is the best choice for EMC DM filter, because has the lowest losses in DC and the highest at the switching frequency. In case of high current is better than the flatwire, except for the higher frequency $f>7MHz$

In [23]:
newplot=LogLogPlotDualAxisPlotly()
newplot.add_trace(f=f, zabs=np.real(Z_flatwire), zdeg=None, label="FlatBandWire")
newplot.add_trace(f=f, zabs=np.real(Z_planar), zdeg=None, label="LitzEmulation")
newplot.add_trace(f=f, zabs=np.real(Z_RFlitz), zdeg=None, label="RF Litz Wire")
newplot.add_trace(f=f, zabs=np.real(Z_SolidStrand), zdeg=None, label="SolidStrand")
newplot.show()
In [14]:
range.xa['Hz'] = f
ya.inputData[0] = Zabs_flatwire
ya.inputData[1] = Zdeg_flatwire

my_model = zfitcli.DoModel()
fit_model = zfitcli.import_module("Models." + "rpcpl")
my_model.do_model_cli(range=range, ya=ya, model=fit_model)
R, 34280.57494368474
C, 3.013975948521488e-12
L, 8.798214023849146e-05
Number of function calls: 49
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 49
    # data points      = 402
    # variables        = 3
    chi-square         = 1.06642924
    reduced chi-square = 0.00267275
    Akaike info crit   = -2378.71875
    Bayesian info crit = -2366.72939
[[Variables]]
    R:  34.2805749 +/- 2.15341762 (6.28%) (init = 1)
    C:  0.03891026 +/- 4.5801e-04 (1.18%) (init = 0.5163978)
    L:  1.13584455 +/- 0.00970322 (0.85%) (init = 1.936492)
[[Correlations]] (unreported correlations are < 0.100)
    C(C, L) = -0.625
In [15]:
newplot=LogLogPlotDualAxisPlotly()
newplot.add_trace(f=f, zabs=Zabs_flatwire, zdeg=Zdeg_flatwire, label="FlatBandWire")
newplot.add_trace(f=f, zabs=my_model.ya.modeledData[0], zdeg=my_model.ya.modeledData[1], label="FIT_FlatBandWire")
newplot.show()
In [ ]: